﻿using System;
using System.Text;
using System.Web;
using System.Web.SessionState;
using System.Configuration;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;

namespace SF.Base.Core
{
    public class CaptchaHandler : IHttpHandler, IRequiresSessionState
    {
        /// <summary>
        /// 您将需要在您网站的 web.config 文件中配置此处理程序，
        /// 并向 IIS 注册此处理程序，然后才能进行使用。有关详细信息，
        /// 请参见下面的链接: http://go.microsoft.com/?linkid=8101007
        /// </summary>
        #region IHttpHandler Members

        public bool IsReusable
        {
            // 如果无法为其他请求重用托管处理程序，则返回 false。
            // 如果按请求保留某些状态信息，则通常这将为 false。
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            //在此写入您的处理程序实现。

            int drawTimes = 0;      //重试过的次数
        NEED_REDRAW:                //如果生成的图形在画布上显示不全，则跳转到此处进行重画,一般应该一次画好，不需要进行跳转

            drawTimes++;
            if (drawTimes > 3)  //防止死循环
            {
                return;
            }

            Bitmap Img = null;
            Graphics g = null;
            MemoryStream ms = null;
            Random random = new Random();

            try
            {
                //不缓存
                context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
                int x, y, x1, y1;

                //建议去掉num这个选择项，因为对坏人来说可以手动指定调用哪项，一项被破就整体被破了，如果需要多套的话可独立实现多套
                string num = context.Request["num"]; //第几种验证码配置 

                //支持多重随机抽取的元素
                string[] fontNameItem = _CodeFontName.Split(','); //字体
                Color[] fontColorItem = GetColors(_CodeFontColor); //字体颜色

                //得到验证码,并将验证字符写入Session供前台调用
                string verifyChar = RndChar(GetSingleConfigString(num, _CodeChar), _CodeLength);
                //HttpContext.Current.Session[CodeSessionID] = verifyChar;
                context.Session[GetSingleConfigString(num, _CodeSessionID)] = verifyChar;

                //验证区高度
                Img = new Bitmap(_ImgWidth, _ImgHeigth);

                //在图片框picCanvas上面建立一个新的空白Graphics
                g = Graphics.FromImage(Img);

                //填充位图背景
                g.FillRectangle(new SolidBrush(_CodeBackgroundColor), 0, 0, Img.Width, Img.Height);


                //字体
                Font f = new System.Drawing.Font(
                    fontNameItem[random.Next(fontNameItem.Length)],
                    _CodeFontSize + random.Next(_CodeFontSizeRandScope),
                    (System.Drawing.FontStyle.Bold));  //使用粗体字体更利于人眼识别，同时提高字体重合度
                //颜色
                Color myColor = fontColorItem[random.Next(fontColorItem.Length)];
                Brush brush = new System.Drawing.SolidBrush(myColor);

                //逐个画出文字
                int left;
                int top = _CodeShowFontTop;
                int lastXX = 0;
                for (int i = 0; i < verifyChar.Length; i++)
                {
                    f = new System.Drawing.Font(
                        fontNameItem[random.Next(fontNameItem.Length)],
                        _CodeFontSize + random.Next(_CodeFontSizeRandScope),
                        (System.Drawing.FontStyle.Bold));  //使用粗体字体更利于人眼识别，同时提高字体重合度

                    int angle = random.Next(2 * _CodeMaxTurnAngle) - _CodeMaxTurnAngle;  //得到随机的旋转角度

                    if (i == 0)
                    {
                        left = _CodeShowFontLeftInit;
                        lastXX = left;
                    }
                    else
                    {
                        left = lastXX + 50;
                        if (left > _ImgWidth || left < 0)
                        {
                            goto NEED_REDRAW;
                        }
                        Bitmap ImgTmp = new Bitmap(_ImgWidth + 50, _ImgHeigth);  //内存
                        Graphics g1 = Graphics.FromImage(ImgTmp);
                        g1.TranslateTransform(left, top);
                        g1.RotateTransform(angle);
                        g1.TranslateTransform(-left, top);
                        g1.DrawString(verifyChar.Substring(i, 1), f, brush, left, top);
                        g1.ResetTransform();

                        int minLineToMove = 50;

                        //对所有行进行计算，计算每一行的间距，最后得到最小值，即需要移动的距离
                        for (int yy = 0; yy < _ImgHeigth; yy += _CodeScanPixTop)
                        {
                            int maxXXtmp = lastXX;
                            int minXXtmp = left + 20;
                            int LineToMove = 0;
                            for (int xx = lastXX; xx < left + 20; xx += _CodeScanPixLeft)    //计算其中一行的间距
                            {
                                if (xx < _ImgWidth && xx > 0)
                                {
                                    if (Img.GetPixel(xx, yy).Equals(myColor)) { maxXXtmp = xx; }    //之前已画部分最右边的x坐标
                                }
                                if (ImgTmp.GetPixel(xx, yy).Equals(myColor) && xx < minXXtmp)
                                {
                                    minXXtmp = xx;    //本次新增部分最左边的坐标
                                }
                            }
                            LineToMove = minXXtmp - maxXXtmp;
                            if (LineToMove < 0)       //如果已经重合，则不再进行移动和下面的计算
                            {
                                LineToMove = 0;
                                minLineToMove = 0;
                                break;
                            }
                            if (minLineToMove > LineToMove)
                            {
                                minLineToMove = LineToMove - 1 - _CodeShowFontSpace; //最终计算出的需要左移的距离
                            }
                        }

                        //上面的循环得到了需要移动的距离minLineToMove
                        left = left - minLineToMove + _CodeFontRealSpace;   //向坐移动到合理的坐标
                        lastXX = left;
                    }

                    //在正式bitmap中画出文字
                    if (left < _ImgWidth && left > 0)
                    {
                        g.TranslateTransform(left, top);
                        g.RotateTransform(angle);
                        g.TranslateTransform(-left, top);
                        g.DrawString(verifyChar.Substring(i, 1), f, brush, left, top);
                        g.ResetTransform();
                    }
                    else
                    {
                        goto NEED_REDRAW;
                    }
                }


                if (_CodeShowMode >= 2)
                {
                    //随机输出噪音线
                    float PenWidth1;
                    for (int i = 0; i < _CodeNoiseLineNumber; i++)
                    {
                        x = 0 + random.Next(Img.Width) / 5; //random.Next(Img.Width);
                        y = random.Next(Img.Height);
                        x1 = Img.Width - random.Next(Img.Width) / 5; // random.Next(Img.Width);
                        y1 = random.Next(Img.Height);
                        //修改参数可获得不同的效果
                        PenWidth1 = 2 + (float)(random.Next(_CodeNoiseLineWidth)) / (100F);
                        g.DrawLine(new Pen(new SolidBrush(myColor), PenWidth1), x, y, x1, y1);
                    }

                    //随机输出噪点(矩形)
                    for (int i = 0; i < _CodeNoiseRectangleNumber; i++)
                    {
                        x = random.Next(Img.Width);
                        y = random.Next(Img.Height);
                        g.DrawRectangle(new Pen(fontColorItem[random.Next(fontColorItem.Length)], 1), x, y, 1, 1);
                    }

                    //画图片的前景噪音点(点像素)
                    for (int i = 0; i < _CodeNoiseDotNumber; i++)
                    {
                        x = random.Next(Img.Width);
                        y = random.Next(Img.Height);
                        Img.SetPixel(x, y, fontColorItem[random.Next(fontColorItem.Length)]);
                    }

                    if (_CodeShowMode >= 3)
                    {
                        //扭曲验证字符。TwistImage参数可自行修改，多扭曲几次也没关系，只是消耗服务器资源多些
                        double Twist1 = random.NextDouble() * 1.0 + 5.0;
                        double Twist2 = random.NextDouble() * 1.0;
                        Img = TwistImage(Img, true, -Twist1, -Twist2);
                        //Img = TwistImage(Img, false, Twist1, Twist2);
                    }

                }

                //画一个边框边框颜色为Color.Gainsboro
                g = Graphics.FromImage(Img);
                g.DrawRectangle(new Pen(_CodeBackgroundColor, 1), 0, 0, Img.Width - 1, Img.Height - 1);



                ms = new MemoryStream();
                Img.Save(ms, ImageFormat.Jpeg); //png兼容性不好，最好还是用jpg格式

                context.Response.ClearContent(); //需要输出图象信息 要修改HTTP头
                context.Response.ContentType = "image/jpeg";
                context.Response.BinaryWrite(ms.ToArray());

                ms.Dispose();
                g.Dispose();
                Img.Dispose();
                //context.Response.End();
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (ms != null) ms.Dispose();
                if (g != null) g.Dispose();
                if (Img != null) Img.Dispose();
            }

        }

        #endregion

        #region 全局变量

        /// <summary>
        /// 下面的值定义建议放到配置文件中，方便灵活配置，但配置时请遵循下面注释中的补充说明
        /// </summary>

        //最重要的四个参数
        private static int _CodeShowMode = 2;                                          //1：只显示文字  2：显示干扰点和干扰线 3：显示干扰点，干扰线并扭曲文字
        private static int _CodeShowFontSpace = -1;                                    //字符之间的间隔距离，间隔距离越小，用户和自动识别程序的识别难度都越高
        private static int _CodeScanPixLeft = 1;                                       //粘连时水平方向每次扫描多少个像素点，可选1,2,3,每次扫描点越多，性能越好，但粘连的精度会降低
        private static int _CodeScanPixTop = 1;                                        //粘连时垂直方向每次扫描多少个像素点，同上，精度越低，用户和自动识别程序的识别难度都越高

        private const double PI = 3.14159265358979;
        private const double PI2 = 6.28318530717959;

        private static string[] _CodeChar = "2345689ABCDEFGHKPQRSUWXYZ".Split('|');    //建议把一些不容易识别的字符去掉，比如0和o,1,i,7等;
        //private static string[] _CodeChar = "2345689".Split('|');
        private static int _CodeLength = 6;                                            //验证码字符数量

        private static int _CodeNoiseLineNumber = 0;                                   //干扰线数量
        private static int _CodeNoiseLineWidth = 20;                                   //干扰线宽度，20-30比较合适
        private static int _CodeNoiseRectangleNumber = 15;                             //干扰矩形数量
        private static int _CodeNoiseDotNumber = 5;                                    //干扰点数量

        //Font Name, 支持多个用,分割, 如Tahoma,Arial
        private static string _CodeFontName = "Arial,Arial Black,TahomaAgency FB,Tahoma,Book Antiqua"; //支持的字体，可根据安装了的字体进行配置，但每添加一种字体进来的时候需要单独测试一下字体的效果
        //Font Size
        private static int _CodeFontSize = 27;                                           //字体大小，这儿只是基数，在这个大小的基础上还会增加一个随机值
        private static string _CodeFontColor = "133FA8,133EA8,233EA8,999999,499699";    //颜色对安全性没有太大影响，主要考虑美观方面
        //Background Color
        private static Color _CodeBackgroundColor = GetColor("EEEEEE");                 //背景色

        //Img Width
        private static int _ImgWidth = 150;  //可配合_CodeLength和字体大小来做相应改变，修改大小后同时记得修改引用的地方
        //Img Heigth
        private static int _ImgHeigth = 50;  //建议50左右，不要太小，否则很难显示清楚旋转后的文字

        //Show Font Position
        private static int _CodeShowFontLeftInit = 2;         //左边距
        private static int _CodeShowFontTop = 1;              //右边距

        private static int _CodeFontSizeRandScope = 3;   //字体大小随机变化的范围，和_CodeFontSize配合使用
        private static int _CodeMaxTurnAngle = 10;  //随机旋转最大角度
        private static int _CodeFontRealSpace = 0; //字体之间的粘连距离，建议取-1或者0，-1表示充分粘连，安全性更高，同时用户识别也更加费劲一些

        /// <summary>
        /// Session ID
        /// </summary>
        public static string[] CodeSessionID { get { return _CodeSessionID; } }
        private static string[] _CodeSessionID = "ValidateCodeSessionID".Split('|'); //Data4NetConfigurationSectionHandler.GetString(ConstantDefine.PRODUCT_NAME + "ValidateCodeSessionID", ConstantDefine.PRODUCT_NAME + "ValidateCodeSessionID").Split('|');
        
        #endregion

        /// <summary>
        /// 产生指定长度的由数字和字符组成的随机字符串
        /// </summary>
        /// <param name="codeChar">设定返回随机字符串所包含哪些字符</param>
        /// <param name="codeNum">设定返回随机字符串的位数</param>
        /// <returns></returns>
        private string RndChar(string codeChar, int codeNum)
        {
            char[] codeCharItem = codeChar.ToCharArray();
            string returnCode = "";
            Random random = new Random();
            for (int i = 0; i < codeNum; i++)
            {
                returnCode += codeCharItem[random.Next(codeCharItem.Length)].ToString();
            }
            return returnCode;
        }
        
        /// <summary>
        /// 正弦曲线Wave扭曲图片。函数可以迭加使用，以获得不同方式不同程度的效果
        /// </summary>
        /// <param name="srcBmp"></param>
        /// <param name="bXDir">扭曲方式</param>
        /// <param name="dMultValue">波形的幅度倍数</param>
        /// <param name="dPhase">波形的起始相位，取值区间[0-2*PI)</param>
        /// <returns></returns>
        private Bitmap TwistImage(Bitmap srcBmp, bool bXDir, double dMultValue, double dPhase)
        {
            Bitmap destBmp = new Bitmap(srcBmp.Width, srcBmp.Height);
            Graphics g = Graphics.FromImage(destBmp);
            g.FillRectangle(new SolidBrush(_CodeBackgroundColor), 0, 0, destBmp.Width, destBmp.Height);

            double dBaseAxisLen = bXDir ? Convert.ToDouble(destBmp.Height) : Convert.ToDouble(destBmp.Width);

            for (int i = 0; i < srcBmp.Width; i++)
            {
                for (int j = 0; j < srcBmp.Height; j++)
                {
                    double dx = bXDir ? PI2 * Convert.ToDouble(j) / dBaseAxisLen : PI2 * Convert.ToDouble(i) / dBaseAxisLen;
                    dx += dPhase;
                    double dy = Math.Sin(dx);

                    //取得当前点的颜色
                    int iOldX = 0, iOldY = 0;
                    iOldX = bXDir ? i + (int)(dy * dMultValue) : i;
                    iOldY = bXDir ? j : j + (int)(dy * dMultValue);

                    Color color = srcBmp.GetPixel(i, j);
                    if (iOldX >= 0 && iOldX < destBmp.Width && iOldY >= 0 && iOldY < destBmp.Height)
                    {
                        destBmp.SetPixel(iOldX, iOldY, color);
                    }
                }
            }
            return destBmp;
        }

        /// <summary>
        /// 得到单个配置的属性
        /// </summary>
        /// <param name="numObj"></param>
        /// <param name="stringItem"></param>
        /// <returns></returns>
        private static string GetSingleConfigString(string numObj, string[] stringItem)
        {
            int num = 0;
            try { num = Convert.ToInt32(numObj); }
            catch { }

            return num >= 0 && num < stringItem.Length ? stringItem[num] : stringItem[0];
        }

        /// <summary>
        /// Get colors
        /// </summary>
        /// <param name="colors"></param>
        /// <returns></returns>
        private static Color[] GetColors(string colors)
        {
            string[] colorValueItem = colors.Split(',');
            Color[] colorItem = new Color[colorValueItem.Length];
            for (int i = 0; i < colorValueItem.Length; i++)
            {
                colorItem[i] = GetColor(colorValueItem[i]);
            }
            return colorItem;
        }

        /// <summary>
        /// Get color
        /// </summary>
        /// <param name="colorValue"></param>
        /// <returns></returns>
        private static Color GetColor(string colorValue)
        {
            Color color = new Color();
            int red = Convert.ToInt32(colorValue.Substring(0, 2), 16);
            int green = Convert.ToInt32(colorValue.Substring(2, 2), 16);
            int blue = Convert.ToInt32(colorValue.Substring(4, 2), 16);
            color = Color.FromArgb(red, green, blue);
            return color;
        }
    }
}
